package it.tukano.beanstore;

import it.tukano.collections.LinkedListExt;
import it.tukano.collections.ListExt;
import it.tukano.fun.Function1;
import it.tukano.fun.OptionalValue;
import it.tukano.io.FileExt;
import it.tukano.io.FileExt.InputStreamCall;
import it.tukano.io.FileExt.OutputStreamCall;
import it.tukano.io.Zipper;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Calendar;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A bean store that saves the beans in a predefined folder
 *
 * @author pgi
 */
public class FileBeanStore implements BeanStore {

    private final File folder;//where the data files are stored
    private final Map<File, CachedBean> cache = new ConcurrentHashMap<File, CachedBean>();

    /**
     * Executes a call that writes data to the storage location identified by
     * the given string
     *
     * @param <T> the type of the value returned by the stream call
     * @param uid the id of the storage location
     * @param call the stream call used to write data
     * @return the value returned by the write call
     */
    public <T> T withBinaryOutput(String uid, OutputStreamCall<T> call) {
	OutputStream out = null;
	T value = null;
	File file = uidToBinaryFile(uid);
	try {
	    out = new FileOutputStream(file);
	    value = call.call(out);
	} catch (Throwable ex) {
	    Logger.getLogger(FileBeanStore.class.getName()).log(Level.SEVERE, null, ex);
	} finally {
	    if (out != null) {
		try {
		    out.close();
		} catch (IOException ex) {
		    Logger.getLogger(FileBeanStore.class.getName()).log(Level.SEVERE, null, ex);
		}
	    }
	}
	return value;
    }

    public <T> T withBinaryInput(String uid, InputStreamCall<T> call) {
	InputStream in = null;
	T value = null;
	File file = uidToBinaryFile(uid);
	try {
	    in = new FileInputStream(file);
	    value = call.call(in);
	} catch (Throwable ex) {
	    Logger.getLogger(FileBeanStore.class.getName()).log(Level.SEVERE, null, ex);
	} finally {
	    if (in != null) {
		try {
		    in.close();
		} catch (IOException ex) {
		    Logger.getLogger(FileBeanStore.class.getName()).log(Level.SEVERE, null, ex);
		}
	    }
	}
	return value;
    }

    public <T> ListExt<T> loadAll(final Class<T> beanClass) throws BeanStoreException {
	final LinkedListExt<T> list = new LinkedListExt<T>();
	foreach(new Function1<Object, Boolean>() {

	    public Boolean apply(Object p) {
		Class<? extends Object> pType = p.getClass();
		if (beanClass.isAssignableFrom(pType)) {
		    list.add(beanClass.cast(p));
		}
		return true;
	    }
	});
	return list;
    }

    public void foreach(String[] keywords, Function1<Object, Boolean> function1) {
	ListExt<FileExt> beanFiles = new FileExt(folder).listFiles(".bean.xml");
	for (FileExt fileExt : beanFiles) {
	    String contentAsString = fileExt.getContentAsString("utf-8").toLowerCase();
	    for (int i = 0; i < keywords.length; i++) {
		String keyword = keywords[i];
		if (contentAsString.contains(keyword.toLowerCase())) {
		    try {
			Object bean = load(fileExt.getFile());
			Boolean goon = function1.apply(bean);
			if (Boolean.FALSE.equals(goon)) {
			    return;
			}
		    } catch (Exception ex) {
			Logger.getLogger(FileBeanStore.class.getName()).log(Level.SEVERE, null, ex);
		    }
		}
	    }
	}
    }

    public void dumpDebug() {
    }

    public void backup() {
	Calendar c = Calendar.getInstance();
	int day = c.get(Calendar.DAY_OF_MONTH); //1-31
	int month = 1 + c.get(Calendar.MONTH);//1-12
	int year = c.get(Calendar.YEAR);
	String dayToken = (day < 10 ? "0" : "") + day;
	String monthToken = (month < 10 ? "0" : "") + month;
	String filename = "_BACKUP" + dayToken + monthToken + year + ".zip";
	File file = new File(folder, filename);
	if (file.exists()) {
	    Logger.getLogger(FileBeanStore.class.getName()).log(Level.INFO, "BACKUP FILE {0} EXISTS", file);
	} else {
	    Zipper zipper = null;
	    try {
		zipper = new Zipper(new FileExt(file));
		File[] archivableFiles = folder.listFiles(new FilenameFilter() {

		    public boolean accept(File dir, String name) {
			name = name.toLowerCase();
			boolean hasPrefix = name.startsWith("_backup");
			boolean hasSuffix = name.endsWith(".zip");
			return (!hasPrefix) && (!hasSuffix);
		    }
		});
		if (archivableFiles != null) {
		    for (int i = 0; i < archivableFiles.length; i++) {
			zipper.zip(new FileExt(archivableFiles[i]));
		    }
		}
	    } finally {
		try {
		    if (zipper != null) {
			zipper.close();
		    }
		} catch (IOException ex) {
		    Logger.getLogger(FileBeanStore.class.getName()).log(Level.SEVERE, null, ex);
		}
	    }
	}
    }

    public static class CachedBean {

	private long lastModified;
	private Object bean;

	public CachedBean(long lastModified, Object bean) {
	    this.lastModified = lastModified;
	    this.bean = bean;
	}

	public Object getBean() {
	    return bean;
	}

	public boolean isUpToDate(File file) {
	    if (file == null) {
		return false;
	    }
	    long fileTime = file.lastModified();
	    return lastModified == fileTime;
	}
    }

    /**
     * Instance initializer
     *
     * @param beanStoreFolder the folder used by this bean store.
     */
    public FileBeanStore(File beanStoreFolder) {
	this.folder = beanStoreFolder;
    }

    public void delete(String uid) {
	File file = uidToBeanFile(uid);
	if (file.exists()) {
	    cache.remove(file);
	    file.delete();
	}
    }

    public void save(String uid, Object bean) {
	File file = uidToBeanFile(uid);
	try {
	    save(file, bean);
	    cache.put(file, new CachedBean(file.lastModified(), bean));
	} catch (IOException ex) {
	    throw new BeanStoreException(ex);
	}
    }

    public OptionalValue<Object> load(String uid) {
	File file = uidToBeanFile(uid);

	CachedBean cachedValue = cache.get(file);
	if (cachedValue != null && cachedValue.isUpToDate(file)) {
	    return new OptionalValue<Object>(cachedValue.getBean());
	}

	try {
	    OptionalValue<Object> v = file == null ? new OptionalValue<Object>(null) : new OptionalValue<Object>(load(file));
	    if (v.isDefined()) {
		cache.put(file, new CachedBean(file.lastModified(), v.get()));
	    }
	    return v;
	} catch (IOException ex) {
	    throw new BeanStoreException(ex);
	}
    }

    public <T> OptionalValue<T> load(String uid, Class<T> beanClass) {
	OptionalValue<Object> op = load(uid);
	return op.isDefined() && beanClass.isAssignableFrom(op.get().getClass())
		? new OptionalValue<T>(beanClass.cast(op.get()))
		: new OptionalValue<T>(null);
    }

    public void save(Object bean) {
	String uid = bean.getClass().getName().replace(".", "_");
	save(uid, bean);
    }

    public <T> OptionalValue<T> load(Class<T> beanClass) {
	String uid = beanClass.getName().replace(".", "_");
	return load(uid, beanClass);
    }

    protected File uidToBeanFile(String uid) {
	return fileNameToFile(uid + ".bean.xml");
    }

    protected File uidToBinaryFile(String uid) {
	return fileNameToFile(uid + ".binary");
    }

    private File fileNameToFile(String fileName) {
	return new File(folder, fileName);
    }

    protected Object load(File file) throws IOException {
	if (!file.exists()) {
	    return null;
	}

	CachedBean cachedValue = cache.get(file);
	if (cachedValue != null && cachedValue.isUpToDate(file)) {
	    return cachedValue.getBean();
	}

	FileInputStream fin = null;
	try {
	    fin = new FileInputStream(file);
	    XMLDecoder decoder = new XMLDecoder(fin);
	    Object obj = decoder.readObject();
	    CachedBean v = new CachedBean(file.lastModified(), obj);
	    cache.put(file, v);
	    return obj;
	} finally {
	    if (fin != null) {
		fin.close();
	    }
	}
    }

    protected void save(File file, Object bean) throws IOException {
	FileOutputStream fout = null;
	try {
	    fout = new FileOutputStream(file);
	    XMLEncoder encoder = new XMLEncoder(fout);
	    encoder.writeObject(bean);
	    encoder.flush();
	    encoder.close();
	} finally {
	    if (fout != null) {
		fout.close();
	    }
	}
    }

    public String generateUid() {
	File file = fileNameToFile("uidbean.bin");
	if (!file.exists()) {
	    try {
		createUidFile(file);
	    } catch (IOException ex) {
		throw new BeanStoreException(ex);
	    }
	}
	try {
	    long uid = readNextUid(file);
	    return String.valueOf(uid);
	} catch (IOException ex) {
	    throw new BeanStoreException(ex);
	}
    }

    private long readNextUid(File uidfile) throws IOException {
	FileChannel channel = null;
	try {
	    channel = new RandomAccessFile(uidfile, "rw").getChannel();
	    FileLock lock = null;
	    try {
		while (lock == null) {
		    lock = channel.tryLock(0, 8, false);
		    if (lock == null) {
			try {
			    Thread.sleep(50);
			} catch (InterruptedException ex) {
			    throw new BeanStoreException(ex);
			}
		    }
		}
		ByteBuffer buffer = ByteBuffer.allocate(8);
		while (buffer.hasRemaining()) {
		    channel.read(buffer);
		}
		buffer.flip();
		long value = buffer.getLong();
		buffer.clear();
		buffer.putLong(value + 1);
		buffer.flip();
		channel.position(0);
		while (buffer.hasRemaining()) {
		    channel.write(buffer);
		}
		channel.force(false);
		return value;
	    } finally {
		if (lock != null) {
		    lock.release();
		}
	    }
	} finally {
	    if (channel != null) {
		channel.close();
	    }
	}
    }

    private void createUidFile(File file) throws IOException {
	FileChannel channel = null;
	try {
	    channel = new FileOutputStream(file).getChannel();
	    FileLock lock = null;
	    try {
		lock = channel.lock(0, 8, false);
		ByteBuffer buffer = ByteBuffer.allocate(8);
		buffer.putLong(0).flip();
		while (buffer.hasRemaining()) {
		    channel.write(buffer);
		}
		channel.force(false);
	    } finally {
		if (lock != null) {
		    lock.release();
		}
	    }
	} finally {
	    if (channel != null) {
		channel.close();
	    }
	}
    }

    private File[] listBeanFiles(File folder) {
	FilenameFilter beanFileFilter = new FilenameFilter() {

	    public boolean accept(File dir, String name) {
		return name.endsWith(".bean.xml");
	    }
	};
	return folder.listFiles(beanFileFilter);
    }

    /**
     * Executes a function for each bean in the store
     *
     * @param op the function to apply to each bean.
     */
    public void foreach(Function1<Object, Boolean> op) {
	File[] files = listBeanFiles(folder);
	if (files != null) {
	    for (File file : files) {
		try {
		    Object bean = load(file);
		    Boolean goon = op.apply(bean);
		    if (!goon) {
			break;
		    }
		} catch (IOException ex) {
		    throw new BeanStoreException(ex);
		}
	    }
	}
    }

    public BufferedOutputStream openBinaryOutput(final String uid) throws IOException {
	File file = uidToBinaryFile(uid);
	return new BufferedOutputStream(new FileOutputStream(file)) {

	    @Override
	    public void close() throws IOException {
		super.close();
	    }
	};
    }

    public BufferedInputStream openBinaryInput(String uid) throws IOException {
	File file = uidToBinaryFile(uid);
	if (!file.exists()) {
	    return null;
	}
	return new BufferedInputStream(new FileInputStream(file));
    }

    public void saveBinary(String uid, byte[] bytes) {
	File file = uidToBinaryFile(uid);
	FileExt fileExt = new FileExt(file);
	try {
	    fileExt.write(bytes);
	} catch (Exception ex) {
	    throw new BeanStoreException(ex);
	}
    }
}
